diff options
Diffstat (limited to 'app/[lng]/evcp')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx | 202 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/basic-contract/page.tsx | 2 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/bid/[id]/page.tsx | 52 |
3 files changed, 255 insertions, 1 deletions
diff --git a/app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx new file mode 100644 index 00000000..c3136496 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/basic-contract/[id]/page.tsx @@ -0,0 +1,202 @@ +import * as React from "react" +import { type SearchParams } from "@/types/table" +import { getValidFilters } from "@/lib/data-table" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { Shell } from "@/components/shell" +import { getBasicContractsByTemplateId, getBasicContractTemplateInfo } from "@/lib/basic-contract/service" +import { searchParamsCacheByTemplateId } from "@/lib/basic-contract/validations" +import { InformationButton } from "@/components/information/information-button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { FileText, Calendar, AlertTriangle, ArrowLeft } from "lucide-react" +import { formatDateTime } from "@/lib/utils" +import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage } from "@/components/ui/breadcrumb" +import { Button } from "@/components/ui/button" +import Link from "next/link" +import { BasicContractsDetailTable } from "@/lib/basic-contract/status-detail/basic-contracts-detail-table" + +interface IndexPageProps { + params: { + id: string + } + searchParams: Promise<SearchParams> +} + +export default async function IndexPage(props: IndexPageProps) { + const resolvedParams = await props.params + const id = resolvedParams.id + const templateId = parseInt(id, 10) + + if (isNaN(templateId)) { + return ( + <Shell> + <div className="text-center py-10"> + <h1 className="text-2xl font-bold text-gray-900">잘못된 템플릿 ID</h1> + <p className="text-gray-500 mt-2">올바른 템플릿을 선택해주세요.</p> + </div> + </Shell> + ) + } + + const searchParams = await props.searchParams + const search = searchParamsCacheByTemplateId.parse(searchParams) + const validFilters = getValidFilters(search.filters) + + // 템플릿 정보 조회 + const templateInfoPromise = getBasicContractTemplateInfo(templateId) + + // 계약서 목록 조회 + const contractsPromise = getBasicContractsByTemplateId( + { + ...search, + filters: validFilters, + }, + templateId + ) + + const promises = Promise.all([contractsPromise]) + + return ( + <Shell className="gap-2"> + {/* 상단 헤더: Breadcrumb (좌) + 뒤로가기 버튼(우) */} + <div className="flex items-center justify-between"> + <Breadcrumb> + <BreadcrumbList> + <BreadcrumbItem> + <BreadcrumbLink href="/evcp">EVCP</BreadcrumbLink> + </BreadcrumbItem> + <BreadcrumbSeparator /> + <BreadcrumbItem> + <BreadcrumbLink href="/evcp/basic-contract">기본계약서/서약서 관리</BreadcrumbLink> + </BreadcrumbItem> + <BreadcrumbSeparator /> + <BreadcrumbItem> + <BreadcrumbPage>기본계약서/서약서 관리 상세</BreadcrumbPage> + </BreadcrumbItem> + </BreadcrumbList> + </Breadcrumb> + + <Button asChild variant="outline" size="sm"> + <Link href="/evcp/basic-contract"> + <ArrowLeft className="h-4 w-4 mr-2" /> + 목록으로 돌아가기 + </Link> + </Button> + </div> + + {/* 템플릿 정보 섹션 (콤팩트) */} + <React.Suspense fallback={<TemplateInfoSkeleton />}> + <TemplateInfo templateInfoPromise={templateInfoPromise} /> + </React.Suspense> + + <Separator /> + + {/* 계약서 리스트 제목 */} + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <FileText className="h-5 w-5" /> + <h2 className="text-xl font-semibold">계약서 목록</h2> + <InformationButton pagePath="partners/basic-contract-detail" /> + </div> + </div> + + {/* 계약서 테이블 */} + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={8} + searchableColumnCount={2} + filterableColumnCount={3} + cellWidths={["3rem", "10rem", "15rem", "8rem", "8rem", "10rem", "10rem", "3rem"]} + shrinkZero + /> + } + > + <BasicContractsDetailTable promises={promises} templateId={templateId} /> + </React.Suspense> + </Shell> + ) +} + +// 템플릿 정보 컴포넌트 (콤팩트 버전) +async function TemplateInfo({ + templateInfoPromise, +}: { + templateInfoPromise: Promise<Awaited<ReturnType<typeof getBasicContractTemplateInfo>>> +}) { + const templateInfo = await templateInfoPromise + + if (!templateInfo) { + return ( + <Card> + <CardContent className="py-6"> + <div className="text-center text-gray-500"> + <AlertTriangle className="h-8 w-8 mx-auto mb-2" /> + <p>템플릿 정보를 찾을 수 없습니다.</p> + </div> + </CardContent> + </Card> + ) + } + + return ( + <Card> + <CardHeader className="py-4"> + <div className="flex flex-wrap items-center justify-between gap-2"> + {/* 좌측: 제목 + 리비전 */} + <div className="flex items-center gap-2 min-w-0"> + <CardTitle className="text-lg font-semibold leading-none truncate"> + {templateInfo.templateName} + </CardTitle> + <Badge variant="outline" className="shrink-0">v{templateInfo.revision}</Badge> + </div> + + {/* 우측: 상태/법무검토 */} + <div className="flex items-center gap-2"> + {templateInfo.legalReviewRequired && ( + <Badge variant="secondary" className="shrink-0">법무검토 필요</Badge> + )} + <Badge + variant={templateInfo.status === "ACTIVE" ? "default" : "secondary"} + className="shrink-0" + > + {templateInfo.status === "ACTIVE" ? "활성" : "비활성"} + </Badge> + </div> + </div> + + {/* 메타 정보 한 줄 정리 */} + <div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> + <span className="inline-flex items-center gap-1"> + <Calendar className="h-3.5 w-3.5" /> + 생성일: {formatDateTime(templateInfo.createdAt, "KR")} + </span> + {templateInfo.fileName && ( + <> + <span className="select-none">•</span> + <span className="inline-flex items-center gap-1 min-w-0"> + <FileText className="h-3.5 w-3.5" /> + <span className="truncate max-w-[52ch]">템플릿 파일: {templateInfo.fileName}</span> + </span> + </> + )} + </div> + </CardHeader> + </Card> + ) +} + +// 로딩 스켈레톤 (콤팩트) +function TemplateInfoSkeleton() { + return ( + <Card> + <CardHeader className="py-4"> + <div className="space-y-2"> + <div className="h-5 bg-gray-200 rounded w-1/2 animate-pulse" /> + <div className="h-3 bg-gray-200 rounded w-1/3 animate-pulse" /> + </div> + </CardHeader> + </Card> + ) +} diff --git a/app/[lng]/evcp/(evcp)/basic-contract/page.tsx b/app/[lng]/evcp/(evcp)/basic-contract/page.tsx index 69a65b14..66b3ee31 100644 --- a/app/[lng]/evcp/(evcp)/basic-contract/page.tsx +++ b/app/[lng]/evcp/(evcp)/basic-contract/page.tsx @@ -36,7 +36,7 @@ export default async function IndexPage(props: IndexPageProps) { <div> <div className="flex items-center gap-2"> <h2 className="text-2xl font-bold tracking-tight"> - 기본계약서 서명 현황 + 기본계약서/서약서 관리 </h2> <InformationButton pagePath="evcp/basic-contract" /> </div> diff --git a/app/[lng]/evcp/(evcp)/bid/[id]/page.tsx b/app/[lng]/evcp/(evcp)/bid/[id]/page.tsx new file mode 100644 index 00000000..e4051f9b --- /dev/null +++ b/app/[lng]/evcp/(evcp)/bid/[id]/page.tsx @@ -0,0 +1,52 @@ +import { Suspense } from 'react' +import { notFound } from 'next/navigation' +import { getBiddingDetailData } from '@/lib/bidding/detail/service' +import { BiddingDetailContent } from '@/lib/bidding/detail/table/bidding-detail-content' + +// 메타데이터 생성 +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const parsedId = parseInt(id) + if (isNaN(parsedId)) return { title: '입찰 상세' } + + try { + const detailData = await getBiddingDetailData(parsedId) + return { + title: detailData.bidding ? `${detailData.bidding.title} - 입찰 상세` : '입찰 상세', + } + } catch { + return { title: '입찰 상세' } + } +} + +interface PageProps { + params: Promise<{ id: string }> +} + +export default async function Page({ params }: PageProps) { + const { id } = await params + const parsedId = parseInt(id) + + if (isNaN(parsedId)) { + notFound() + } + + // 통합 데이터 로딩 함수 사용 + const detailData = await getBiddingDetailData(parsedId) + + if (!detailData.bidding) { + notFound() + } + + return ( + <Suspense fallback={<div className="p-8">로딩 중...</div>}> + <BiddingDetailContent + bidding={detailData.bidding} + quotationDetails={detailData.quotationDetails} + quotationVendors={detailData.quotationVendors} + biddingCompanies={detailData.biddingCompanies} + prItems={detailData.prItems} + /> + </Suspense> + ) +} |
